/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.relations.merge;

import java.util.Date;
import java.util.Objects;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.context.MergeFromRelationRequestContext;
import org.unidata.mdm.data.context.MergeToRelationRequestContext;
import org.unidata.mdm.data.context.RelationIdentityContext;
import org.unidata.mdm.data.context.RelationMergeContext;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.impl.RelationComposerComponent;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.data.type.keys.RelationEtalonKey;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.data.type.merge.MergeRelationMasterState;
import org.unidata.mdm.data.type.timeline.RelationTimeline;
import org.unidata.mdm.meta.type.RelativeDirection;
import org.unidata.mdm.system.type.pipeline.PipelineInput;
import org.unidata.mdm.system.type.pipeline.Point;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;

/**
 * @author Mikhail Mikhailov on Nov 10, 2019
 */
@Component(RelationMergeTimelineExecutor.SEGMENT_ID)
public class RelationMergeTimelineExecutor extends Point<PipelineInput> {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RELATION_MERGE_TIMELINE]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".relation.merge.timeline.description";
    /**
     * The composer.
     */
    @Autowired
    private RelationComposerComponent relationComposerComponent;
    /**
     * Constructor.
     * @param id
     * @param description
     */
    public RelationMergeTimelineExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void point(PipelineInput ctx) {

        MeasurementPoint.start();
        try {

            RelationMergeContext mCtx = (RelationMergeContext) ctx;
            if (mCtx.getDirection() == RelativeDirection.FROM) {
                calculateFromTimeline((MergeFromRelationRequestContext) ctx);
            } else {
                calculateToTimeline((MergeToRelationRequestContext) ctx);
            }

        } finally {
            MeasurementPoint.stop();
        }
    }

    private void calculateFromTimeline(MergeFromRelationRequestContext ctx) {

        MeasurementPoint.start();
        try {

            Date ts = ctx.timestamp();
            String user = SecurityUtils.getCurrentUserName();

            // M2M + Contains
            RelationKeys currentKeys = ctx.relationKeys(); // The duplicate rel key
            Timeline<OriginRelation> currentTimeline = ctx.currentTimeline();
            RecordKeys master = ctx.masterKeys();

            // Master state
            MergeRelationMasterState state = ctx.masterState();

            RelationKeys existingMasterKeys = state.getMasterKeysByTypeAndToId(currentKeys);
            if (Objects.nonNull(existingMasterKeys)) {

                RelationKeys nextKeys = RelationKeys.builder(existingMasterKeys)
                        .supplementaryKeys(currentKeys.getSupplementaryKeys())
                        .updateDate(ctx.timestamp())
                        .updatedBy(SecurityUtils.getCurrentUserName())
                        .build();

                Timeline<OriginRelation> masterTimeline = state.getCurrentFromTimeline(existingMasterKeys);
                Timeline<OriginRelation> next = currentTimeline.merge(masterTimeline);

                next.setKeys(nextKeys);
                next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i, ts, user, ctx.isIncludeWinners()));

                ctx.nextTimeline(next);

                // Update state
                state.putFromTimeline(next);
            } else {

                // Timeline and new keys
                RelationKeys nextKeys = RelationKeys.builder(currentKeys)
                        .etalonKey(RelationEtalonKey.builder(currentKeys.getEtalonKey())
                                    .from(master.getEtalonKey())
                                    .build())
                        .updateDate(ctx.timestamp())
                        .updatedBy(SecurityUtils.getCurrentUserName())
                        .build();

                Timeline<OriginRelation> next = new RelationTimeline(nextKeys, currentTimeline.getCalculables());
                next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i, ts, user, ctx.isIncludeWinners()));

                ctx.nextTimeline(next);

                // Update state
                state.putFromTimeline(next);
            }

        } finally {
            MeasurementPoint.stop();
        }
    }

    private void calculateToTimeline(MergeToRelationRequestContext ctx) {

        MeasurementPoint.start();
        try {

            Date ts = ctx.timestamp();
            String user = SecurityUtils.getCurrentUserName();

            // M2M + Contains
            RelationKeys currentKeys = ctx.relationKeys(); // The duplicate rel key
            Timeline<OriginRelation> currentTimeline = ctx.currentTimeline();
            RecordKeys master = ctx.masterKeys();

            // Master state
            MergeRelationMasterState state = ctx.masterState();

            RelationKeys existingMasterKeys = state.getMasterKeysByTypeAndFromId(currentKeys);
            if (Objects.nonNull(existingMasterKeys)) {

                RelationKeys nextKeys = RelationKeys.builder(existingMasterKeys)
                        .supplementaryKeys(currentKeys.getSupplementaryKeys())
                        .updateDate(ctx.timestamp())
                        .updatedBy(SecurityUtils.getCurrentUserName())
                        .build();

                Timeline<OriginRelation> masterTimeline = state.getCurrentToTimeline(existingMasterKeys);
                Timeline<OriginRelation> next = currentTimeline.merge(masterTimeline);

                next.setKeys(nextKeys);
                next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i, ts, user, ctx.isIncludeWinners()));

                ctx.nextTimeline(next);

                // Update state
                state.putToTimeline(next);
            } else {

                // Timeline and new keys
                RelationKeys nextKeys = RelationKeys.builder(currentKeys)
                        .etalonKey(RelationEtalonKey.builder(currentKeys.getEtalonKey())
                                .to(master.getEtalonKey())
                                .build())
                        .updateDate(ctx.timestamp())
                        .updatedBy(SecurityUtils.getCurrentUserName())
                        .build();

                Timeline<OriginRelation> next = new RelationTimeline(nextKeys, currentTimeline.getCalculables());
                next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i, ts, user, ctx.isIncludeWinners()));

                ctx.nextTimeline(next);

                // Update state
                state.putFromTimeline(next);
            }

        } finally {
            MeasurementPoint.stop();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(Start<?, ?> start) {
        return RelationIdentityContext.class.isAssignableFrom(start.getInputTypeClass())
            && RelationMergeContext.class.isAssignableFrom(start.getInputTypeClass());
    }
}
